import type { ResourceKey } from '@warp-drive/core/types';
import type { Diagnostic, Hooks } from '@warp-drive/diagnostic/-types';

import type { CacheOperation, NotificationType } from '@warp-drive/core';
import type { RequestKey } from '@warp-drive/core/types/identifier';

import { configureAssertAllDeprecations } from './assert-all-deprecations';
import { configureAssertionHandler } from './assert-assertion';
import { configureBetterAsserts } from './assert-better';
import { configureDeprecationHandler, DeprecationConfig, FoundDeprecation } from './assert-deprecation';
import { configureNotificationsAssert } from './assert-notification';
import { configureWarningHandler, WarningConfig } from './assert-warning';
import type { RenderingTestContext } from '@warp-drive/diagnostic/ember';

declare module '@warp-drive/diagnostic/-types' {
  export interface Diagnostic {
    /**
     * Compat with QUnit assert interface
     */
    test: {
      expected: number;
    };

    expectDeprecation(options: DeprecationConfig, label?: string): void;
    expectDeprecation(
      callback: () => void | Promise<void>,
      options: DeprecationConfig | string | RegExp,
      label?: string
    ): Promise<void>;
    expectNoDeprecation(
      callback: () => void | Promise<void>,
      label?: string,
      filter?: (deprecation: FoundDeprecation) => boolean
    ): Promise<void>;
    expectWarning(callback: () => unknown, options: WarningConfig | string | RegExp): Promise<void>;
    expectNoWarning(callback: () => unknown): Promise<void>;
    expectAssertion(callback: () => unknown, matcher: string | RegExp): Promise<void>;
    expectNoAssertion(callback: () => unknown): Promise<void>;
    watchNotifications(store?: unknown): void;

    /**
     * Asserts that each member of actual strictly matches the corresponding member of expected.
     * Asserts that actual is an array and has the same length as expected.
     */
    arrayStrictEquals<T>(actual: unknown, expected: T[], message: string): void;
    /**
     * Asserts that the given identifier has been notified of a change to the given bucket
     * and optional key the given number of times during the test.
     *
     * Clears the notification count for the given identifier, bucket and key after the assertion
     * is made so that it is easy to assert notification counts in between steps of a test.
     */
    notified(
      identifier: RequestKey | ResourceKey,
      bucket: NotificationType | CacheOperation,
      key: string | null,
      count: number,
      message?: string
    ): void;

    clearNotifications(): void;
  }
}

export interface ExpandedHooks {
  onSuiteStart: (cb: () => void | Promise<void>) => void;
  onSuiteFinish: (cb: () => void | Promise<void>) => void;
  beforeModule: (cb: () => void | Promise<void>) => void;
  afterModule: (cb: () => void | Promise<void>) => void;
  beforeEach: (cb: (assert: Diagnostic) => void | Promise<void>) => void;
  afterEach: (cb: (assert: Diagnostic) => void | Promise<void>) => void;
}

interface NestedHooks {
  /**
   * Runs after the last test. If additional tests are defined after the
   * module's queue has emptied, it will not run this hook again.
   */
  after(fn: (assert: Assert) => void | Promise<void>): void;

  /**
   * Runs after each test.
   */
  afterEach(fn: (assert: Assert) => void | Promise<void>): void;

  /**
   * Runs before the first test.
   */
  before(fn: (assert: Assert) => void | Promise<void>): void;

  /**
   * Runs before each test.
   */
  beforeEach(fn: (assert: Assert) => void | Promise<void>): void;
}

function upgradeHooks(hooks: Hooks | NestedHooks): ExpandedHooks {
  const upgraded = hooks as unknown as ExpandedHooks;
  // eslint-disable-next-line no-restricted-globals
  const Framework = typeof QUnit !== 'undefined' ? QUnit : null;
  if (Framework) {
    // eslint-disable-next-line @typescript-eslint/unbound-method
    upgraded.onSuiteStart = Framework.begin;
    // eslint-disable-next-line @typescript-eslint/unbound-method
    upgraded.onSuiteFinish = Framework.done;
    // eslint-disable-next-line @typescript-eslint/unbound-method
    upgraded.beforeModule = (hooks as NestedHooks).before;
    // eslint-disable-next-line @typescript-eslint/unbound-method
    upgraded.afterModule = (hooks as NestedHooks).after;
  }
  return upgraded;
}

export default function configureAsserts(
  hooks: Hooks | NestedHooks,
  options?: { assertAllDeprecations: boolean }
): void {
  const upgraded = upgradeHooks(hooks);

  upgraded.beforeEach(function (this: RenderingTestContext, assert) {
    configureAssertionHandler(assert);
    configureDeprecationHandler(assert);
    configureWarningHandler(assert);
    configureBetterAsserts(assert);
    configureNotificationsAssert.call(this, assert);
  });
  if (options?.assertAllDeprecations ?? true) {
    configureAssertAllDeprecations(upgraded);
  }
}
